<?php

namespace App\Console\Commands;

use Facebook\WebDriver\Chrome\ChromeDriver;
use Facebook\WebDriver\Chrome\ChromeOptions;
use Facebook\WebDriver\Cookie;
use Facebook\WebDriver\Remote\DesiredCapabilities;
use Facebook\WebDriver\WebDriverBy;
use Facebook\WebDriver\WebDriverExpectedCondition;
use GuzzleHttp\Client;
use GuzzleHttp\Cookie\CookieJar;
use GuzzleHttp\Cookie\SetCookie;
use GuzzleHttp\Exception\RequestException;
use Illuminate\Console\Command;
use League\HTMLToMarkdown\HtmlConverter;
use PHPHtmlParser\Dom;
use Psr\Http\Message\ResponseInterface;

class CourseDl extends Command
{
    const GET_CHAPTERS_SCRIPT = "
return (() => {
    let items = document.querySelectorAll('body > div.pusher > div.main.container > div > div > div > div > ol > li > ol > li > a');
    let result = [];
    for (let a of items) {
        result.push({'title': a.firstChild.textContent.trim(), 'link': a.href});
    }
    return JSON.stringify(result);
})();";

    /**
     * @var string $ua
     */
    private $ua;

    /**
     * @var Cookie[] $cookies
     */
    private $cookies;

    /**
     * The name and signature of the console command.
     *
     * @var string
     */
    protected $signature = 'course:dl {url=https://learnku.com/courses/laravel-shop/5.5 : The course url}';

    /**
     * The console command description.
     *
     * @var string
     */
    protected $description = 'Download learnku course';

    /**
     * Create a new command instance.
     *
     * @return void
     */
    public function __construct()
    {
        parent::__construct();
    }

    /**
     * Execute the console command.
     *
     * @return mixed
     */
    public function handle()
    {
        $start = time();
        $url = $this->argument('url');
        $split = mb_split('learnku.com/courses/', $url);
        if (2 != count($split)) {
            $this->error('url error');
            return;
        }
        $courseName = $split[1]; // course name and version, will be used as directory name

        $this->info("\n^_^ It may take a while, please be patient. ^_^\n");
        $driver = $this->initChrome();
        try {
            if (!$this->login($driver)) {
                $this->error('login failed');
            }
            usleep(mt_rand(20e5, 35e5));  // wait 2 ~ 3.5 s
            $driver->get($url);
            $this->ua = $driver->executeScript('return navigator.userAgent;');
            $this->cookies = $driver->manage()->getCookies(); // Hold cookies

            // Server-side rendered, don't have to wait document is ready
            // Like as [['title' => 'a', 'link' => 'https://'],]
            $chapterArr = json_decode($driver->executeScript(self::GET_CHAPTERS_SCRIPT), true);
            app('log')->info('chapter', compact('url', 'chapterArr'));
            usleep(mt_rand(1e6, 2e6));  // wait 1 ~ 2 s

//            $link = 'https://learnku.com/courses/laravel-shop/5.5/about-the-author/1531'; // Do not need login
//            $link = 'https://learnku.com/courses/laravel-shop/5.5/user-interface-collection-of-goods-list/1596'; // Need login
            $cache = app('cache');
            $expiresAt = 5 * 60 + time();
//            $referer = $url;
//            foreach ($chapterArr as $chapter) {
//                $title = $chapter['title'];
//                $link = $chapter['link'];
//                list($content,) = $this->getPageContent($link, $referer);
//                $referer = $link;
//                $result = $this->html2MdTurndown($content);
////            $this->line($result);
//                $cache->put($title, $result, $expiresAt); // Just write, not read anywhere
//                $filename = str_replace('/', '-', $title);
//
//                if (!$this->write2File("$courseName/$filename.html", $content)) {
//                    app('log')->error('on save html', compact('filename', 'link'));
//                }
//                if (trim($result) && !$this->write2File("$courseName/$filename.md", $result)) {
//                    app('log')->error('on save md', compact('filename', 'link'));
//                }
//                usleep(mt_rand(25e5, 45e5)); // wait 2.5 ~ 4.5 s
//            }

            if (0 == count($chapterArr)) {
                $this->error('get chapter structure error');
                return;
            }
            $driver->get($chapterArr[0]['link']);
            foreach ($chapterArr as $chapter) {
                $title = $chapter['title'];
                $link = $chapter['link'];
                list($content, ,) = $this->clickPage($driver, $link);
                $result = $this->html2MdTurndown($content);
                $cache->put($title, $result, $expiresAt); // Just write, not read anywhere
                $filename = str_replace('/', '-', $title);

                if (!$this->write2File("$courseName/$filename.html", $content)) {
                    app('log')->error('on save html', compact('filename', 'link'));
                }
                if (trim($result) && !$this->write2File("$courseName/$filename.md", $result)) {
                    app('log')->error('on save md', compact('filename', 'link'));
                }
            }
        } catch (\Exception $e) {
            app('log')->error($e);
            $this->error($e->getMessage());
        } finally {
            $driver->close();
            $cost = time() - $start;
            $this->info("--- DONE ---\ncost: $cost s");
        }
    }

    /**
     * Init and start the browser
     *
     * @return ChromeDriver
     */
    private function initChrome(): ChromeDriver
    {
        putenv('webdriver.chrome.driver=' . env('CHROME_DRIVER', shell_exec('which chromedriver')));
        $caps = DesiredCapabilities::chrome();
        $options = new ChromeOptions();
        $options->addArguments([
            '--window-size=1920,1080',
            '--disable-gpu',
            '--headless',
            '--no-sandbox',
            '--disable-dev-shm-usage',
            '--mute-audio',
        ]);
        $caps->setCapability(ChromeOptions::CAPABILITY, $options);
        return ChromeDriver::start($caps);
    }

    /**
     * Login user account
     *
     * @param $driver
     *
     * @return bool
     */
    private function login($driver): bool
    {
        $name = env('LEARNKU_USERNAME');
        $pwd = env('LEARNKU_PASSWORD');
        if (!$name || !$pwd) {
            $this->error('username or password not set');
            return false;
        }
        $url = 'https://learnku.com/auth/login';
        $driver->get($url);
        usleep(1000000); // 1 s
//        $username = WebDriverBy::cssSelector('input[name=username]');
//        $password = WebDriverBy::cssSelector('input[name=password]');
//        getElement($driver, $username)->sendKeys($name);
//        getElement($driver, $password)->sendKeys($pwd);
        $submitScript = "
(() => {
    document.querySelector('form.login input[name=username]').value = '$name';
    document.querySelector('form.login input[name=password]').value = '$pwd';
    document.querySelector('form.login').submit();
})();";
        $driver->executeScript($submitScript);
        $driver->wait(5, 250)
            ->until(WebDriverExpectedCondition::not(
                WebDriverExpectedCondition::urlIs($url)), 'login timeout');
        $this->cookies = $driver->manage()->getCookies();

        return true;
    }

    /**
     * Get each chapter content by browser
     *
     * @param        $driver
     * @param string $url
     *
     * @return array
     */
    private function clickPage($driver, $url): array
    {
        $script = "
return (() => {
    let content = '';
    let purchased = false;
    let contentDiv = document.querySelector('.article-content');
    let purchaseI = document.querySelector('.article-content a.green i.shop');
    content =  contentDiv && contentDiv.outerHTML;
    purchased = purchaseI && !purchaseI.parentElement.innerText.includes('购买');
    return [content, purchased];
})();";
        list($content, $purchased) = $driver->executeScript($script);
        $forwardScript = "
return (() => {
    let nextBtn = document.querySelector('.article-content + div a.button.article-pager-btn.pull-right.popover');
    nextBtn && nextBtn.click();
    return null == nextBtn;
})();";
        usleep(mt_rand(25e5, 45e5)); // wait 2.5 ~ 4.5 s
        $hasNextPage = $driver->executeScript($forwardScript); // Fake a real referer
        if ($hasNextPage) {
            $driver->wait(5, 250)
                ->until(WebDriverExpectedCondition::not(
                    WebDriverExpectedCondition::urlIs($url)), 'forward timeout');
        }
        return [$content, $purchased, $hasNextPage];
    }

    /**
     * Get each chapter content by request
     *
     * @param string $url
     * @param string $referer
     *
     * @deprecated DOM parser render problem
     *
     * @return array
     */
    private function getPageContent($url, $referer): array
    {
        $content = '';
        $purchased = false;
        // Prepare cookies
        $cookieJar = new CookieJar();
        foreach ($this->cookies as $cookie) {
            $setCookie = new SetCookie(array_change_key_capital_case($cookie->toArray())); // Well done
            $cookieJar->setCookie($setCookie);
        }

        $client = new Client([
            'base_uri' => $url,
            'headers' => [
                'Referer' => $referer,
                'X-PJAX' => true,
                'X-PJAX-Container' => 'body',
                'X-Requested-With' => 'XMLHttpRequest',
                'User-Agent' => $this->ua,
            ],
            'cookies' => $cookieJar,
        ]);
        $client->requestAsync('get', null, [
            'query' => [
                '_pjax' => 'body',
            ],
        ])->then(function (ResponseInterface $res) use (&$content, &$purchased) {
            $page = $res->getBody()->getContents();
            $purchased = $this->checkIfPurchased($page);
            // Parse DOM and query element
            // FIXME There is a problem when render the 'pre' element
            // WONTFIX so, the solution will be deprecated
            $dom = (new Dom())->load($page);
            $selector = '.article-content';
            $content = $dom->find($selector)->outerHtml();
        }, function (RequestException $e) {
            app('log')->error($e);
        })->wait();

        return [$content, $purchased];
    }

    /**
     * Check if is purchased
     *
     * @param $content
     *
     * @return bool
     */
    private function checkIfPurchased($content): bool
    {
        return false == strpos($content, '付费后</a> 才能观看剩余内容');
    }

    /**
     * Convert html to markdown
     *
     * @param string $html
     *
     * @deprecated no appropriate convert solutions
     *
     * @return string
     */
    private function html2Md($html): string
    {
        static $converter;
        if (null == $converter) {
            $converter = new HtmlConverter([
                'strip_tags' => true,
                'remove_nodes' => 'div',
            ]);
        }
        return $converter->convert($html);
    }

    /**
     * Convert html to markdown using turndown
     *
     * @param string $html
     *
     * @return string
     */
    private function html2MdTurndown($html): string
    {
        $md = '';
        $url = env('TURNDOWN_SERVICE', 'http://127.0.0.1:9999');
        $client = new Client([
            'base_uri' => $url,
        ]);
        $client->requestAsync('post', null, [
            'body' => $html,
        ])->then(function (ResponseInterface $res) use (&$md, &$html) {
            if (200 == $res->getStatusCode()) {
                $md = (string)$res->getBody();
            } else {
                app('log')->error('TURNDOWN_SERVICE', [$res->getReasonPhrase()]);
            }
        }, function (RequestException $e) {
            app('log')->error($e);
        })->wait();

        return $md;
    }

    /**
     * Convert html to markdown online
     *
     * @param string $html
     *
     * @deprecated not recommended
     *
     * @return string
     */
    private function html2MdOnline($html): string
    {
        $result = '';
        $url = 'https://domchristie.github.io/turndown/';
        $driver = $this->initChrome();
        $driver->get($url);
        $script = "
(() => {
    document.querySelector('#headingStyle').selectedIndex = 1;
    document.querySelector('#hr').selectedIndex = 1;
    document.querySelector('#bulletListMarker').selectedIndex = 0;
    document.querySelector('#codeBlockStyle').selectedIndex = 1;
    document.querySelector('#fence').selectedIndex = 0;
    document.querySelector('#emDelimiter').selectedIndex = 1;
    document.querySelector('#strongDelimiter').selectedIndex = 0;
    document.querySelector('#linkStyle').selectedIndex = 0;
    document.querySelector('#linkReferenceStyle').selectedIndex = 0;
    document.querySelector('#input').value = '$html';
    document.querySelector('#options').dispatchEvent(new Event('change'));
    return document.querySelector('#output').value;
})();";
        try {
            $result = $driver->executeScript($script);
        } catch (\Exception $e) {
            app('log')->error($e);
            $this->error($e->getMessage());
        } finally {
            $driver->close();
        }
        return $result;
    }

    /**
     * @param string $filename with path
     * @param string $contents The file contents.
     *
     * @return bool
     */
    private function write2File($filename, $contents): bool
    {
        $b = false;
        try {
            $b = app('filesystem')->disk('local')->write($filename, $contents);
        } catch (\Exception $e) {
            app('log')->info('write2file failed', compact('e'));
        }
        return $b;
    }
}
